Skip to content

Polymorphism

In order to build usable, accessible interfaces, it's important that we understand the semantics of different HTML tags.

For example: if an element can be clicked to perform an action in JS, it should be a button! Unless that action is to navigate the user to a new page, in which case, it should be an anchor (<a>).

(If you'd like to learn more about why adding a click-handler to a <div> is such a bad idea, I recommend checking out React Podcast #34, “Just Use A Button”, with superstar developer Jen Luker. The entire episode is worth listening to, but the button-specific part is around 26:30.)

When choosing an HTML tag, it's much more important to focus on the semantics than the aesthetics. You should use a <button> even if you don't want it to look like a button. With CSS, we can strip away all of those built-in button styles. It's much easier to remove a handful of CSS rules than it is to recreate all of the usability benefits built into the <button> tag.

With all of that in mind, let's suppose our designer wants us to build the following UI:

Mockup of a dashboard showing a list of transactions, with some actions in the top right

In the top right, there are some actions the user can take:

Zoom-in of the previous image, showing 3 actions: “Add Transaction”, “View Report”, and “Export All Data”

These look like links, but are they? It depends on whether clicking them changes the URL or not. “Export All Data” doesn't sound like a link to me; I imagine it generating a .csv and emailing it to the user.

So, here's what we're going to do. We're going to build a LinkButton component. It's always going to look like a link, but it's going to be flexible in its implementation: it can either render an <a> tag, or a <button> tag, depending on whether an href is supplied.

Spend a few minutes tinkering, and then watch the video below to see how I'd approach this problem.

Acceptance Criteria:

  • The LinkButton component has an optional prop, href.
  • If an href is provided, LinkButton should render an <a> tag. Otherwise, it should render a <button> tag.

Code Playground

import React from 'react';

import styles from './LinkButton.module.css';

function LinkButton({ href, children }) {
// TODO: render an <a> tag if “href” is provided.
return (
<button className={styles.button}>
{children}
</button>
)
}

export default LinkButton;

Let's explore:

Video Summary

So there's a few ways we could solve this problem.

We could create a separate "branch" based on the href:

function LinkButton({
href,
children,
}) {
// Branch 1: anchor
if (href) {
return (
<a
href={href}
className={styles.button}
>
{children}
</a>
);
}
// Branch 2: button
return (
<button className={styles.button}>
{children}
</button>
);
}

This works, but it's a bit of a bummer. Whenever I make a change to this component, I have to remember to edit both branches. For example, adding a ...delegated object:

function LinkButton({
href,
children,
...delegated
}) {
// Branch 1: anchor
if (href) {
return (
<a
href={href}
className={styles.button}
{...delegated}
>
{children}
</a>
);
}
// Branch 2: button
return (
<button
className={styles.button}
{...delegated}
>
{children}
</button>
);
}

Instead, I prefer to solve this problem using a technique known as polymorphism.

The first time you see this approach, it looks a little funky, but fear not! I'll explain everything.

First, here's what it looks like:

function LinkButton({
href,
children,
...delegated
}) {
const Tag = typeof href === 'string'
? 'a'
: 'button';
return (
<Tag
href={href}
className={styles.button}
{...delegated}
>
{children}
</Tag>
);
}

To understand what's going on here, it's helpful to look at the plain JS one, without the JSX making it harder to understand:

// This code:
React.createElement(
Tag,
{
href,
className: styles.button,
...delegated
},
children
);
// Is the same as this code:
<Tag
href={href}
className={styles.button}
{...delegated}
>
{children}
</Tag>

The Tag variable will resolve either to the string "a" or "button". The type is dynamic, but either way, it's a string.

It's confusing because we generally reserve uppercase variable names for components like App or Slider. So it's weird that an uppercase variable is rendering a "native" DOM node.

Why does Tag have to be uppercase? Wouldn't it be more natural to make our variable "tag" instead?

Well, if we try that, we'll render literal <tag> HTML elements. 😬

<tag href="/add-transaction" class="button">
Add Transaction
</tag>

As we learned in Module 1, lowercase JSX tags are treated verbatim, while capitalized JSX tags are treated dynamically:

<Button/> // React.createElement(Button)
<button/> // React.createElement('button')

We need to use a capital letter so that the JSX compiler creates an element using the Tag variable, and not the "tag" string.

I know that this pattern is a bit of a headscratcher the first time you see it, but I think it's a pretty elegant solution to a tough problem.

Here's the final code from the video: